//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************



using System;
using System.Collections.Generic;
using System.Text;
using System.CodeDom.Compiler;
using System.IO;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace Common
{


    /// <summary>
    /// 
    /// A TestCase represents a C# source file that defines a test
    /// case generated by Randoop. It has several properties, which
    /// are specified as comments in the source code.
    /// 
    /// * LAST ACTION: the last method/constructor/field access
    ///                in the test case.
    /// 
    /// * EXCEPTION: the exception type that is thrown when the
    ///              test case executes.
    /// 
    /// * ASSEMBLIES: names of the assemblies that must be
    ///               references for the test case to compile.
    /// 
    /// * IMPORTS: namespaces required for the test case to compile.
    /// 
    /// * CLASSNAME: name of the class defined in the test case.
    /// 
    /// * TESTCODE: code that comprises the actual tests.
    /// 
    /// Below is an example of what a TestCase looks like as a file. 
    /// 
    /// TODO update
    /// //LAST ACTION: NodeType
    /// //EXCEPTION: System.FormatException
    /// using System.Xml;
    /// using System;
    /// public class RandoopTestCase3102
    /// {
    ///    public static void Test()
    ///    {
    ///        try
    ///        {
    ///           //BEGIN TEST
    ///           System.Xml.XmlDocument v0 = new System.Xml.XmlDocument();
    ///           System.Xml.XmlNodeType v14 = ((System.Xml.XmlDocument)v13).NodeType;
    ///           //END TEST                
    ///        }
    ///        catch (System.Exception e)
    ///        {
    ///            System.Console.Error.WriteLine("// EXCEPTION:"
    ///            + e.GetType().FullName);
    ///        }
    ///    }
    /// }
    /// //REFASSEMBLY: System.Xml.dll
    /// 
    /// </summary>
    public class TestCase
    {
        public const string BEGIN_TEST_MARKER = "//BEGIN TEST";
        public const string END_TEST_MARKER = "//END TEST";
        public LastAction lastAction;
        public ExceptionDescription exception;
        private Collection<string> imports;
        private Collection<RefAssembly> refAssemblies;
        private string className;
        private Collection<string> testCode;

        private static ExceptionDescription assertionViolation = ExceptionDescription.GetDescription(typeof(Common.AssertionViolation));

        /// <summary>
        /// Create a TestCase by specifying all its components.
        /// </summary>
        public TestCase(LastAction lastAction, ExceptionDescription exception, Collection<string> imports,
            Collection<string> assemblies, string className, Collection<string> testCode)
        {
            // Last action.
            if (lastAction == null) throw new ArgumentNullException();
            this.lastAction = lastAction;

            // Exception.
            if (exception == null) throw new ArgumentNullException();
            this.exception = exception;

            // Imports.
            if (imports == null) throw new ArgumentNullException();
            this.imports = new Collection<string>();
            foreach (string s in imports)
            {
                if (s == null) throw new ArgumentNullException();
                this.imports.Add(s);
            }

            // Referenced assemblies.
            this.refAssemblies = new Collection<RefAssembly>();
            foreach (string s in assemblies)
            {
                if (s == null) throw new ArgumentNullException();
                this.refAssemblies.Add(new RefAssembly(RefAssembly.REFASSEMBLY_MARKER + s));
            }

            // Class name.
            if (className == null) throw new ArgumentNullException();
            this.className = className;

            // Test code.
            if (testCode == null) throw new ArgumentNullException();
            this.testCode = new Collection<string>();
            foreach (string s in testCode)
            {
                if (s == null) throw new ArgumentNullException();
                this.testCode.Add(s);
            }
        }

        /// <summary>
        /// Creates a TestCase given a file.
        /// IMPORTANT: KEEP IN SYNC WITH TestCase.Write().
        /// </summary>
        public TestCase(FileInfo filePath)
        {
            StreamReader reader = null;
            try
            {
                reader = new StreamReader(filePath.FullName);
            }
            catch (Exception e)
            {
                Common.Enviroment.Fail("Encountered a problem when attempting to read file "
                    + filePath
                    + ". Exception message: "
                    + e.Message);
            }

            string line;

            try
            {
                // Read lastAction.
                line = reader.ReadLine();
                if (line == null || !line.StartsWith(LastAction.LASTACTION_MARKER)) throw new TestCaseParseException(filePath);
                line = line.Trim();
                this.lastAction = new LastAction(line);

                // Read exception.
                line = reader.ReadLine();
                if (line == null || !line.StartsWith(ExceptionDescription.EXCEPTION_DESCRIPTION_MARKER)) throw new TestCaseParseException(filePath);
                line = line.Trim();
                this.exception = new ExceptionDescription(line);

                // Read imports.
                this.imports = new Collection<string>();
                while (((line = reader.ReadLine()) != null) && line.Trim().StartsWith("using"))
                {
                    this.imports.Add(line.Trim());
                }

                // Read class name.
                if (line == null || !line.Trim().StartsWith("public class")) throw new TestCaseParseException(filePath);
                this.className = line.Trim().Substring("public class".Length);

                // Read open bracket (class).
                line = reader.ReadLine();
                if (line == null || !line.Trim().Equals("{")) throw new TestCaseParseException(filePath);

                // Main method declaration line.
                line = reader.ReadLine();
                line = line.Trim();
                if (line == null || !line.StartsWith("public static int")) throw new TestCaseParseException(filePath);
                if (line == null || !line.EndsWith("()")) throw new TestCaseParseException(filePath);

                // Read open bracket (method).
                line = reader.ReadLine();
                if (line == null || !line.Trim().Equals("{")) throw new TestCaseParseException(filePath);

                // Read "try".
                line = reader.ReadLine();
                if (line == null || !line.Trim().Equals("try")) throw new TestCaseParseException(filePath);

                // Read open bracket (try).
                line = reader.ReadLine();
                if (line == null || !line.Trim().Equals("{")) throw new TestCaseParseException(filePath);

                // Read testCode.
                this.testCode = new Collection<string>();
                line = reader.ReadLine();
                if (line == null || !line.Trim().Equals(BEGIN_TEST_MARKER)) throw new TestCaseParseException(filePath);
                while (((line = reader.ReadLine()) != null) && !line.Trim().Equals(END_TEST_MARKER))
                {
                    this.testCode.Add(line.Trim());
                }
                if (line == null || !line.Trim().Equals(END_TEST_MARKER)) throw new TestCaseParseException(filePath);

                // Everything that remains to read are the assemblies referenced.

                // Read assemblies referenced.
                this.refAssemblies = new Collection<RefAssembly>();
                while (((line = reader.ReadLine()) != null))
                {
                    if (line.Trim().StartsWith(RefAssembly.REFASSEMBLY_MARKER))
                    {
                        this.refAssemblies.Add(new RefAssembly(line.Trim()));
                    }
                }
            }
            finally
            {
                reader.Close();
            }
        }

        public IEnumerable<string> Imports
        {
            get { return this.imports; }
        }

        /// <summary>
        /// Writes this test case to a file.
        /// </summary>
        public void WriteToFile(string filePath, bool declareMainMethod)
        {
            using(var w = new StreamWriter(filePath))
                Write(w);
        }

        /// <summary>
        /// Writes this test case to a file.
        /// </summary>
        public void WriteToFile(FileInfo filePath, bool declareMainMethod)
        {
            using(var w = new StreamWriter(filePath.FullName))
                Write(w);
        }

        /// <summary>
        /// Writes this test case to a file.
        /// IMPORTANT: KEEP IN SYNC WITH TestCase(string filePath).
        /// </summary>
        public void Write(TextWriter w)
        {
            w.WriteLine(this.lastAction.ToString());
            w.WriteLine(this.exception.ToString());

            foreach (string s in this.imports)
            {
                w.WriteLine(s);
            }
            w.WriteLine("public class " + this.className);
            w.WriteLine("{");
            w.WriteLine("  public static int Main()");
            w.WriteLine("  {");
            w.WriteLine("    try");
            w.WriteLine("    {");
            w.WriteLine("      " + BEGIN_TEST_MARKER);
            foreach (string s in this.testCode)
            {
                w.WriteLine("      " + s);
            }
            w.WriteLine("      " + END_TEST_MARKER);

            if (this.exception.IsNoException())
            {
                w.WriteLine("      System.Console.WriteLine(\"This was expected behavior. Will exit with code 100.\");");
                w.WriteLine("      return 100;");
            }
            else
            {
                w.WriteLine("      System.Console.WriteLine(\"This was unexpected behavior (expected an exception). Will exit with code 99.\");");
                w.WriteLine("      return 99;");
            }
            w.WriteLine("    }");
            // TODO the two clauses below mean the same thing. pick one and make sure it's used throughout.
            if (!this.exception.IsNoException() && !this.exception.Equals(assertionViolation))
            {
                w.WriteLine("    catch (" + this.exception.exceptionDescriptionstring + " e)");
                w.WriteLine("    {");
                w.WriteLine("      System.Console.WriteLine(\""
                        + ExceptionDescription.EXCEPTION_DESCRIPTION_MARKER + "\" + e.GetType().FullName);");
                w.WriteLine("      System.Console.WriteLine(\"This was expected behavior. Will exit with code 100.\");");
                w.WriteLine("      return 100;");
                w.WriteLine("    }");
            }

            if (!this.exception.Equals(ExceptionDescription.GetDescription(typeof(Exception))))
            {
                w.WriteLine("    catch (System.Exception e)");
                w.WriteLine("    {");
                w.WriteLine("      System.Console.WriteLine(\""
                        + ExceptionDescription.EXCEPTION_DESCRIPTION_MARKER + "\" + e.GetType().FullName);");
                w.WriteLine("      System.Console.WriteLine(\"//STACK TRACE:\");");
                w.WriteLine("      System.Console.WriteLine(e.StackTrace);");
                w.WriteLine("      System.Console.WriteLine();");
                w.WriteLine("      System.Console.WriteLine(\"This was unexpected behavior. Will exit with code 99.\");");
                w.WriteLine("      return 99;");
                w.WriteLine("    }");
            }
            w.WriteLine("  }");
            w.WriteLine("}");

            // Print referenced assemblies at the end
            // to avoid too much ugliness at top of file.
            foreach (RefAssembly ra in this.refAssemblies)
            {
                w.WriteLine(ra.ToString());
            }

            w.Flush();
            w.Close();
        }

        /// <summary>
        /// Writes this test case to a writer as a unit test.
        /// Expects indentation already in place
        /// </summary>
        public void WriteAsUnitTest(IndentedTextWriter w, string testName)
        {
            w.WriteLine("[TestMethod]");
            // TODO: expected exception
            w.WriteLine("public void {0}()", testName);
            w.WriteLine("{");
            w.Indent++;
            foreach (string s in this.testCode)
                w.WriteLine(s);
            w.Indent--;
            w.WriteLine("}");
        }

        public override string ToString()
        {
            StringBuilder b = new StringBuilder();
            StringWriter w = new StringWriter(b);
            Write(w);
            return b.ToString();
        }

        public string RemoveLine(int line)
        {
            string retval = this.testCode[line];
            this.testCode.RemoveAt(line);
            return retval;
        }


        public int NumTestLines
        {
            get
            {
                return this.testCode.Count;
            }
        }

        public void AddLine(int line, string oldLine)
        {
            this.testCode.Insert(line, oldLine);
        }

        public static TestCase Dummy(string className)
        {
            LastAction lastAction = new LastAction(LastAction.LASTACTION_MARKER + "none");
            ExceptionDescription exception = ExceptionDescription.NoException();
            Collection<string> imports = new Collection<string>();
            Collection<string> assemblies = new Collection<string>();
            Collection<string> testCode = new Collection<string>();
            testCode.Add("/* Source code printing failed. */");
            return new TestCase(lastAction, exception, imports, assemblies, className, testCode);
        }

        /// <summary>
        /// Used to communicate the result of running a test case
        /// as a separate process. See RunExternal().
        /// </summary>
        public class RunResults
        {
            public readonly CompilFailure compilationFailureReason;

            /// <summary>
            /// If !compilationSuccessful, contains the compiler errors.
            /// Otherwise, set to null.
            /// </summary>
            public readonly CompilerErrorCollection compilerErrors;
            public readonly bool compilationSuccessful;
            public readonly bool behaviorReproduced;

            public enum CompilFailure { MissingReference, Other, NA }

            private RunResults(bool compilationSuccessful, bool behaviorReproduced,
                CompilFailure reason, CompilerErrorCollection compilerErrors)
            {
                this.compilationFailureReason = reason;
                this.compilationSuccessful = compilationSuccessful;
                this.behaviorReproduced = behaviorReproduced;
                this.compilerErrors = compilerErrors;
            }

            public static RunResults CompilationFailed(CompilFailure reason, CompilerErrorCollection errors)
            {
                return new RunResults(false, false, reason, errors);
            }

            public static RunResults CompiledOKBehaviorWasReproduced()
            {
                return new RunResults(true, true, CompilFailure.NA, null);
            }

            public static RunResults CompiledOKBehaviorNotReproduced()
            {
                return new RunResults(true, false, CompilFailure.NA, null);
            }
        }

        /// <summary>
        /// Executes this test case in a separate process.
        /// Compares the result of the execution against the
        /// recorded behavior.
        /// 
        /// Returns a description of the execution
        /// (e.g. "behavior preserved" or "behavior not preserved"
        /// or "compilation failed").
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        public RunResults RunExternal()
        {
            // Set up compiler parameters.
            CompilerParameters cp = new CompilerParameters();
            AddReferenceLibraries(cp);
            cp.GenerateExecutable = true;
            cp.OutputAssembly = "Temp.exe";
            cp.GenerateInMemory = false;
            cp.TreatWarningsAsErrors = false;

            // Compile sources.
            CodeDomProvider provider = new Microsoft.CSharp.CSharpCodeProvider();
            CompilerResults cr = provider.CompileAssemblyFromSource(cp, this.ToString());
            if (cr.Errors.Count > 0)
                return AppropriateErrorResult(cr);

            // Run test in separate process.
            Process p = new Process();
            p.StartInfo.FileName = Common.Enviroment.MiniDHandler;
            p.StartInfo.RedirectStandardOutput = true;
            StringBuilder arguments = new StringBuilder();
            arguments.Append("/O:\"C:\\foobar.txt\"");
            arguments.Append(" /I:" + "\"" + Common.Enviroment.DefaultDhi + "\"");
            arguments.Append(" /App:\"Temp\"");
            p.StartInfo.Arguments = arguments.ToString();
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.ErrorDialog = true;
            p.StartInfo.CreateNoWindow = false;

            p.Start();
            string output = p.StandardOutput.ReadToEnd();
            p.WaitForExit(10000);

            // Exit code 100 means behavior was reproduced.
            if (p.ExitCode != 100)
            {
                return RunResults.CompiledOKBehaviorNotReproduced();
            }
            return RunResults.CompiledOKBehaviorWasReproduced();
        }

        private static RunResults AppropriateErrorResult(CompilerResults cr)
        {
            foreach (CompilerError error in cr.Errors)
            {
                if (error.ToString().Contains("is defined in an assembly that is not referenced"))
                {
                    //Console.WriteLine("@@@" + error.ToString());
                    //Console.WriteLine("@@@" + this.ToString());
                    return RunResults.CompilationFailed(RunResults.CompilFailure.MissingReference, cr.Errors);
                }
            }
            return RunResults.CompilationFailed(RunResults.CompilFailure.Other, cr.Errors);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        public void AddReferenceLibraries(CompilerParameters cp)
        {
            bool addedSystem = false;
            bool addedSystemXml = false;
            bool addedSystemSecurity = false;
            bool addedSystemData = false;
            bool addedSystemDrawing = false;
            foreach (RefAssembly ra in this.refAssemblies)
            {
                cp.ReferencedAssemblies.Add(ra.refAssemblyString);

                String assemblyName = ra.refAssemblyString.ToLower();
                if (assemblyName.EndsWith("system.dll"))
                    addedSystem = true;
                else if (assemblyName.EndsWith("system.xml.dll"))
                    addedSystemXml = true;
                else if (assemblyName.EndsWith("system.security.dll"))
                    addedSystemSecurity = true;
                else if (assemblyName.EndsWith("system.data.dll"))
                    addedSystemData = true;
                else if (assemblyName.EndsWith("system.drawing.dll"))
                    addedSystemDrawing = true;
            }

            // Add some standard assemblies by default.
            if (!addedSystem)
                cp.ReferencedAssemblies.Add("System.dll");
            if (!addedSystemXml)
                cp.ReferencedAssemblies.Add("System.Xml.dll");
            if (!addedSystemSecurity)
                cp.ReferencedAssemblies.Add("System.Security.dll");
            if (!addedSystemData)
                cp.ReferencedAssemblies.Add("System.Data.dll");
            if (!addedSystemDrawing)
                cp.ReferencedAssemblies.Add("System.Drawing.dll");
        }



        public class LastAction
        {
            public static readonly string LASTACTION_MARKER = "//LAST ACTION:";
            public readonly string lastActionstring;

            /// <summary>
            /// The given string must be of the form LastAction.LASTACTION_MARKER + description-string.
            /// </summary>

            public LastAction(string s)
            {
                if (s == null) throw new ArgumentNullException("s");
                s = s.Trim();
                if (!s.StartsWith(LASTACTION_MARKER))
                    throw new ArgumentException("Last action description must start with "
                        + LASTACTION_MARKER);
                this.lastActionstring = s.Substring(LASTACTION_MARKER.Length).Trim();
            }

            public override string ToString()
            {
                return LASTACTION_MARKER + " " + lastActionstring;
            }

            public override bool Equals(object obj)
            {
                LastAction other = obj as LastAction;
                if (other == null) return false;
                return other.lastActionstring == this.lastActionstring;

            }

            public override int GetHashCode()
            {
                return this.lastActionstring.GetHashCode();
            }
        }

        public class RefAssembly
        {
            public static readonly string REFASSEMBLY_MARKER = "//REFASSEMBLY:";
            public readonly string refAssemblyString;

            /// <summary>
            /// The given string must be of the form RefAssembly.REFASSEMBLY_MARKER + description-string.
            /// </summary>

            public RefAssembly(string s)
            {
                if (s == null) throw new ArgumentNullException("s");
                s = s.Trim();
                if (!s.StartsWith(REFASSEMBLY_MARKER))
                    throw new ArgumentException("Ref assembly line must start with "
                        + REFASSEMBLY_MARKER);
                this.refAssemblyString = s.Substring(REFASSEMBLY_MARKER.Length).Trim();
            }

            public override string ToString()
            {
                return REFASSEMBLY_MARKER + " " + refAssemblyString;
            }

            public override bool Equals(object obj)
            {
                RefAssembly other = obj as RefAssembly;
                if (other == null) return false;
                return other.refAssemblyString == this.refAssemblyString;

            }

            public override int GetHashCode()
            {
                return this.refAssemblyString.GetHashCode();
            }
        }

        [Serializable]
        public class TestCaseParseException : Exception
        {
            public TestCaseParseException(FileInfo testFile)
                : base(testFile.FullName)
            {
                // No body.
            }
        }


        public class ExceptionDescription
        {
            public static readonly string EXCEPTION_DESCRIPTION_MARKER = "//EXCEPTION:";
            private static readonly string NO_EXCEPTION_string = "none";
            public readonly string exceptionDescriptionstring;

            public string ExceptionDescriptionString
            {
                get
                {
                    return exceptionDescriptionstring;
                }
            }

            public static ExceptionDescription NoException()
            {
                return new ExceptionDescription(EXCEPTION_DESCRIPTION_MARKER + " " + NO_EXCEPTION_string);
            }

            public bool IsNoException()
            {
                return exceptionDescriptionstring.Equals(NO_EXCEPTION_string);
            }

            /// <summary>
            /// Get the description corresponding to the given type.
            /// </summary>
            /// <param name="exceptionType">The type of the exception. Cannot be null.</param>
            /// <returns></returns>
            public static ExceptionDescription GetDescription(Type exceptionType)
            {
                if (exceptionType == null) throw new ArgumentNullException("exceptionType");
                return new ExceptionDescription(ExceptionDescription.EXCEPTION_DESCRIPTION_MARKER
                    + SourceCodePrinting.ToCodeString(exceptionType));
            }

            /// <summary>
            /// The given string must be of the form ExceptionDescription.EXCEPTION_DESCRIPTION_MARKER + description-string.
            /// </summary>
            internal ExceptionDescription(string s)
            {
                if (s == null) throw new ArgumentNullException();
                s = s.Trim();
                if (!s.StartsWith(EXCEPTION_DESCRIPTION_MARKER)) throw new ArgumentException("malformed string s");
                this.exceptionDescriptionstring = s.Substring(EXCEPTION_DESCRIPTION_MARKER.Length).Trim();
            }

            public override string ToString()
            {
                return EXCEPTION_DESCRIPTION_MARKER + " " + exceptionDescriptionstring;
            }

            public override bool Equals(object obj)
            {
                ExceptionDescription other = obj as ExceptionDescription;
                if (other == null) return false;
                return other.exceptionDescriptionstring == this.exceptionDescriptionstring;
            }

            public override int GetHashCode()
            {
                return this.exceptionDescriptionstring.GetHashCode();
            }
        }
    }
}
